package in.co.codedoc.rpc;

import in.co.codedoc.cg.annotations.IsAnIOCComponent;
import in.co.codedoc.ioc.Initializable;
import in.co.codedoc.json.JSONObjectValue;
import in.co.codedoc.json.JSONUtil;
import in.co.codedoc.json.JSONValue;

import in.co.codedoc.user.Authorization;
import in.co.codedoc.util.ObjectUtil;
import in.co.codedoc.util.PropertiesUtil;
import in.co.codedoc.util.StringUtil;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;

@IsAnIOCComponent
public class RPCUtil
	implements Initializable
{
	@Override
	public void Initialize()
	{
		ScanClasspath();
	}
	
	public RPCUtil(JSONUtil jsonUtil)
	{
	}
	
	public static RPCMethodData GetRPCMethod(String name)
	{
		return rpcMethodByName.get(name);
	}
	
	public static synchronized void ScanClasspath()
	{
		rpcMethodByName = new HashMap<String, RPCMethodData>();
		PropertiesUtil.ScanClassPath
		(
			"rpc-adapters.properties",
			new PropertiesUtil.ForEach()
			{
				@Override
				public void Do(String key, String value)
				{
					try
					{
						RegisterRPCExportingClass(key);
					}
					catch(Throwable th)
					{
						Logger.logger.error
						(
							"Failed to register RPC Methods of " + key + ":" + th.getMessage(),
							th
						);
					}
				}

			}
		);
	}

	@SuppressWarnings("unchecked")
	private static void RegisterRPCExportingClass(String rpcAdapterTypeName)
	{
		Logger.logger.debug("Exporting methods from " + rpcAdapterTypeName + " ...");
		
		try
		{
			Class mapperType = Class.forName(rpcAdapterTypeName);

			for(Method method:mapperType.getMethods())
			{
				RPCMethod rpcMethodAnnotation
					= method.getAnnotation(RPCMethod.class);
				
				if(rpcMethodAnnotation != null)
				{
					Class[] parameterTypes = method.getParameterTypes();
					if(parameterTypes.length != 1)
					{
						throw new RuntimeException
						(
							"RPC Methods can take only one parameter : the parameters as a JSON Object Value. " + 
							method.getDeclaringClass().getCanonicalName() + "." +  method.toString() + 
							" does not comply with this requirement."
						);
					}

					String rpcMethodName = rpcMethodAnnotation.name();
					
					if(rpcMethodName == null || rpcMethodName.equals(""))
					{
						rpcMethodName = method.getName();
					}
					
					if(rpcMethodByName.containsKey(rpcMethodName))
					{
						throw new RuntimeException
						(
							"RPC Method name " + rpcMethodName + " is already mapped to " +
							rpcMethodByName.get(rpcMethodName).GetMethod().getDeclaringClass() + "." + 
							rpcMethodByName.get(rpcMethodName).GetMethod().toString()
						);
					}
					else
					{
						Logger.logger.debug("Exporting " + method + "(" + rpcMethodAnnotation + ") as " + rpcMethodName);
						
						RPCMethodData rpcMethodData = new RPCMethodData();
						rpcMethodData.SetName(rpcMethodName);
						rpcMethodData.SetMethod(method);
						rpcMethodData.SetParameterNames(rpcMethodAnnotation.parameterNames().split(","));
						rpcMethodData.SetUser(rpcMethodAnnotation.user());
						rpcMethodData.SetRoles(new HashSet<String>());
						if(rpcMethodAnnotation.roles()!= null && !rpcMethodAnnotation.roles().isEmpty())
						{
							for(String role:rpcMethodAnnotation.roles().split(","))
							{
								rpcMethodData.GetRoles().add(role);
							}
						}						
						rpcMethodByName.put(rpcMethodName,rpcMethodData);
					}
				}				
			}
		}
		catch(Throwable th)
		{
			throw new RuntimeException
			(
				"Failed to register RPC Adapter " + rpcAdapterTypeName + ":" + th.getMessage(),
				th
			);
		}
	}
	
	public static abstract class ForEach
	{
		public abstract void Do(int index,String methodName,String[] parameterNames,Method method); 
	}
	
	public static void ForEach(ForEach forEach)
	{
		int index = 0;
		for(String methodName:rpcMethodByName.keySet())
		{
			RPCMethodData rpcMethodData = rpcMethodByName.get(methodName);
			forEach.Do(index++,methodName,rpcMethodData.GetParameterNames(),rpcMethodData.GetMethod());
		}
	}

	public static JSONValue ExecuteService(String service,JSONValue arguments)
	{
		service = StringUtil.StripQuotesAndEscapes(service);
		
		try
		{
			RPCMethodData rmd = RPCUtil.GetRPCMethod(service);
			
			if(rmd == null)
			{
				throw new RuntimeException("Service " + service + " is not defined.");
			}

			Authorization auth = Authorization.GetCurrentAuthorization();

			if((rmd.GetUser() != null) && !rmd.GetUser().equals("<guest>"))
			{
				boolean authorized = false;
				if(auth != null && (rmd.GetUser().equals("") || rmd.GetUser().equals(auth.GetName())))
				{
					authorized = true;
				}

				if(!authorized)
				{
					if(Logger.logger.isDebugEnabled())
					{
						Logger.logger.debug("A valid user is required to execute " + rmd.GetName());
					}
					throw new RuntimeException("No authorization to execute this service. REFUSED.");
				}
			}
			
			if((rmd.GetRoles() != null) && !(rmd.GetRoles().isEmpty()))
			{
				boolean authorized = false;
				if(auth != null && (auth.GetRoles() != null) && (auth.GetRoles().length > 0))
				{
					OUTER:
					for(String role:auth.GetRoles())
					{
						if(rmd.GetRoles().contains(role))
						{
							authorized = true;
							break OUTER;
						}
					}
				}
				
				if(!authorized)
				{
					if(Logger.logger.isDebugEnabled())
					{
						Logger.logger.debug("A valid role is required to execute " + rmd.GetName() + ". Required:" + rmd.GetRoles() + ". Present:" + ObjectUtil.toString(auth.GetRoles()));
					}
					throw new RuntimeException("No authorization to execute this service. REFUSED.");
				}
			}
			
			return (JSONValue)rmd.GetMethod().invoke(null,new Object[]{(JSONObjectValue)arguments});
		}
		catch(Throwable th)
		{
			Logger.logger.error
			(
				"Exception while invoking service:" + service + 
				". Args:" + arguments.Encode() + ". Exception:" + th.getMessage(),
				th
			);
			
			throw new RuntimeException(th);
		}
	}
	
	private static HashMap<String,RPCMethodData> rpcMethodByName;
}
